/******************************************************************
*
*    CyberUPnP for Java
*
*    Copyright (C) Satoshi Konno 2002-2004
*
*    File: ControlPoint.java
*
*    Revision:
*
*    11/18/02
*        - first revision.
*    05/13/03
*        - Changed to create socket threads each local interfaces.
*          (HTTP, SSDPNotiry, SSDPSerachResponse)
*    05/28/03
*        - Changed to send m-serach packets from SSDPSearchResponseSocket.
*          The socket doesn't bind interface address.
*        - SSDPSearchResponsSocketList that binds a port and a interface can't
*          send m-serch packets of IPv6 on J2SE v 1.4.1_02 and Redhat 9.
*    07/23/03
*        - Suzan Foster (suislief)
*        - Fixed a bug. HOST field was missing.
*    07/29/03
*        - Synchronized when a device is added by the ssdp message.
*    09/08/03
*        - Giordano Sassaroli <sassarol@cefriel.it>
*        - Problem : when an event notification message is received and the message
*                    contains updates on more than one variable, only the first variable update
*                    is notified.
*        - Error :  the other xml nodes of the message are ignored
*        - Fix : add two methods to the NotifyRequest for extracting the property array
*                and modify the httpRequestRecieved method in ControlPoint
*    12/12/03
*        - Added a static() to initialize UPnP class.
*    01/06/04
*        - Added the following methods to remove expired devices automatically
*          removeExpiredDevices()
*          setExpiredDeviceMonitoringInterval()/getExpiredDeviceMonitoringInterval()
*          setDeviceDisposer()/getDeviceDisposer()
*    04/20/04
*        - Added the following methods.
*          start(String target, int mx) and start(String target).
*    06/23/04
*        - Added setNMPRMode() and isNMPRMode().
*    07/08/04
*        - Added renewSubscriberService().
*        - Changed start() to create renew subscriber thread when the NMPR mode is true.
*    08/17/04
*        - Fixed removeExpiredDevices() to remove using the device array.
*    10/16/04
*        - Oliver Newell <newell@media-rush.com>
*        - Added this class to allow ControlPoint applications to be notified when
*          the ControlPoint base class adds/removes a UPnP device
*    03/30/05
*        - Changed addDevice() to use Parser::parse(URL).
*    04/12/06
*        - Added setUserData() and getUserData() to set a user original data object.
*
*******************************************************************/

package com.fiberhome.remoteime.cybergarage.unnp;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;


import com.fiberhome.remoteime.cybergarage.http.HTTPRequest;
import com.fiberhome.remoteime.cybergarage.http.HTTPRequestListener;
import com.fiberhome.remoteime.cybergarage.http.HTTPServerList;
import com.fiberhome.remoteime.cybergarage.net.HostInterface;
import com.fiberhome.remoteime.cybergarage.unnp.control.RenewSubscriber;
import com.fiberhome.remoteime.cybergarage.unnp.device.DeviceChangeListener;
import com.fiberhome.remoteime.cybergarage.unnp.device.Disposer;
import com.fiberhome.remoteime.cybergarage.unnp.device.NotifyListener;
import com.fiberhome.remoteime.cybergarage.unnp.device.ST;
import com.fiberhome.remoteime.cybergarage.unnp.device.SearchResponseListener;
import com.fiberhome.remoteime.cybergarage.unnp.device.USN;
import com.fiberhome.remoteime.cybergarage.unnp.event.EventListener;
import com.fiberhome.remoteime.cybergarage.unnp.event.NotifyRequest;
import com.fiberhome.remoteime.cybergarage.unnp.event.Property;
import com.fiberhome.remoteime.cybergarage.unnp.event.PropertyList;
import com.fiberhome.remoteime.cybergarage.unnp.event.Subscription;
import com.fiberhome.remoteime.cybergarage.unnp.event.SubscriptionRequest;
import com.fiberhome.remoteime.cybergarage.unnp.event.SubscriptionResponse;
import com.fiberhome.remoteime.cybergarage.unnp.ssdp.SSDP;
import com.fiberhome.remoteime.cybergarage.unnp.ssdp.SSDPNotifySocketList;
import com.fiberhome.remoteime.cybergarage.unnp.ssdp.SSDPPacket;
import com.fiberhome.remoteime.cybergarage.unnp.ssdp.SSDPSearchRequest;
import com.fiberhome.remoteime.cybergarage.unnp.ssdp.SSDPSearchResponseSocketList;
import com.fiberhome.remoteime.cybergarage.util.Debug;
import com.fiberhome.remoteime.cybergarage.util.ListenerList;
import com.fiberhome.remoteime.cybergarage.util.Mutex;
import com.fiberhome.remoteime.cybergarage.xml.Node;
import com.fiberhome.remoteime.cybergarage.xml.NodeList;
import com.fiberhome.remoteime.cybergarage.xml.Parser;
import com.fiberhome.remoteime.cybergarage.xml.ParserException;
import com.fiberhome.remoteime.hisilicon.multiscreen.protocol.utils.LogTool;

public class ControlPoint implements HTTPRequestListener
{
    private final static int DEFAULT_EVENTSUB_PORT = 8058;
    private final static int DEFAULT_SSDP_PORT = 8008;
    private final static int DEFAULT_EXPIRED_DEVICE_MONITORING_INTERVAL = 60;

    private final static String DEFAULT_EVENTSUB_URI = "/evetSub";

    ////////////////////////////////////////////////
    //    Member
    ////////////////////////////////////////////////

    private SSDPNotifySocketList ssdpNotifySocketList;
    private SSDPSearchResponseSocketList ssdpSearchResponseSocketList;

    private SSDPNotifySocketList getSSDPNotifySocketList()
    {
        return ssdpNotifySocketList;
    }

    private SSDPSearchResponseSocketList getSSDPSearchResponseSocketList()
    {
        return ssdpSearchResponseSocketList;
    }

    ////////////////////////////////////////////////
    //    Initialize
    ////////////////////////////////////////////////

    static
    {
        UPnP.initialize();
    }

    ////////////////////////////////////////////////
    //    Constructor
    ////////////////////////////////////////////////

    public ControlPoint(int ssdpPort, int httpPort,InetAddress[] binds){
        ssdpNotifySocketList = new SSDPNotifySocketList(binds);
        ssdpSearchResponseSocketList = new SSDPSearchResponseSocketList(binds);

        setSSDPPort(ssdpPort);
        setHTTPPort(httpPort);

        setDeviceDisposer(null);
        setExpiredDeviceMonitoringInterval(DEFAULT_EXPIRED_DEVICE_MONITORING_INTERVAL);

        setRenewSubscriber(null);

        setNMPRMode(false);
        setRenewSubscriber(null);
    }

    public ControlPoint(int ssdpPort, int httpPort){
        this(ssdpPort,httpPort,null);
    }

    public ControlPoint()
    {
        this(DEFAULT_SSDP_PORT, DEFAULT_EVENTSUB_PORT);
    }

    public void finalize()
    {
        stop();
    }

    ////////////////////////////////////////////////
    // Mutex
    ////////////////////////////////////////////////

    private Mutex mutex = new Mutex();

    public void lock()
    {
        mutex.lock();
    }

    public void unlock()
    {
        mutex.unlock();
    }

    ////////////////////////////////////////////////
    //    Port (SSDP)
    ////////////////////////////////////////////////

    private int ssdpPort = 0;

    public int getSSDPPort() {
        return ssdpPort;
    }

    public void setSSDPPort(int port) {
        ssdpPort = port;
    }

    ////////////////////////////////////////////////
    //    Port (EventSub)
    ////////////////////////////////////////////////

    private int httpPort = 0;

    public int getHTTPPort() {
        return httpPort;
    }

    public void setHTTPPort(int port) {
        httpPort = port;
    }

    ////////////////////////////////////////////////
    //    NMPR
    ////////////////////////////////////////////////

    private boolean nmprMode;

    public void setNMPRMode(boolean flag)
    {
        nmprMode = flag;
    }

    public boolean isNMPRMode()
    {
        return nmprMode;
    }

    ////////////////////////////////////////////////
    //    Device List
    ////////////////////////////////////////////////

    private NodeList devNodeList = new NodeList();

    private void addDevice(Node rootNode)
    {
        // Begin modify for MultiScreen.
        devNodeList.lock();
        devNodeList.add(rootNode);
        devNodeList.unlock();
        // End modify for MultiScreen.
    }

    private synchronized void addDevice(SSDPPacket ssdpPacket)
    {
        // Begin modify for MultiScreen. remove ssdpPacket.isRootDevice() == false
        if (ssdpPacket.isHiMultiScreenDevice() == false)
        {
            // CN:多屏UPNP上下线通知和M-SEARCH的响应NT都含ST.MULTISCREEN_DEVICE，不是这个设备类型则不处理。
            // ST.MULTISCREEN_DEVICE = "urn:schemas-upnp-org:device:HiMultiScreenServerDevice:1"
            // CN:虽然通知含有RootDevice，但和DLNA上线通知相同，为提高性能，不处理该通知。
            return;
        }

        // Debug.message("ztest ssdpPacket.getNT()=" + ssdpPacket.getNT());
        // Debug.message("ztest ssdpPacket.getST()=" + ssdpPacket.getST());
        // Debug.message("ztest ssdpPacket.getUSN()=" + ssdpPacket.getUSN());

        String locationURL = ssdpPacket.getLocation();
        String RemoteAddress = ssdpPacket.getRemoteAddress();
        if (!locationURL.contains(RemoteAddress))
        {
            // CN:数据包地址和下载地址不一致，STB SoftAP开启时会出现这种情况。
            Debug.message("Wrong download URL: locationURL=" + locationURL + ", RemoteAddress="
                + RemoteAddress);
            //modify for softap and eth co-exit
            //return;
        }
        // End modify for MultiScreen.

        String usn = ssdpPacket.getUSN();
        String udn = USN.getUDN(usn);
        Device dev = getDevice(udn);
        if (dev != null) {
            dev.setSSDPPacket(ssdpPacket);
            performRefreshDeviceListener( dev );
            return;
        }

        String location = ssdpPacket.getLocation();
        try {
            URL locationUrl = new URL(location);
            Parser parser = UPnP.getXMLParser();
            Node rootNode = parser.parse(locationUrl);
            // Begin modify for MultiScreen.
            // TODO CN:NodeList 加锁
            Device rootDev = getDevice(rootNode);
            if (rootDev == null)
                return;
            if (rootDev.isDeviceType(ST.MULTISCREEN_DEVICE) == false)
            {
                // CN:通知类型是NT: upnp:rootdevice，非多屏设备不加入NodeList进行维护。
                return;
            }
            rootDev.setSSDPPacket(ssdpPacket);
            addDevice(rootNode);
            // TODO CN:NodeList解锁
            // End modify for MultiScreen.

            // Thanks for Oliver Newell (2004/10/16)
            // After node is added, invoke the AddDeviceListener to notify high-level
            // control point application that a new device has been added. (The
            // control point application must implement the DeviceChangeListener interface
            // to receive the notifications)
            performAddDeviceListener( rootDev );
        }
        catch (MalformedURLException me) {
            Debug.warning(ssdpPacket.toString());
            Debug.warning(me);
        }
        catch (ParserException pe) {
            Debug.warning(ssdpPacket.toString());
            Debug.warning(pe);
        }
    }

    /**
     * Add device by URL.<br>
     * @param locationURL URL of description.xml on STB.
     * @param uuid UUID of device.
     * @param localAddress local IP address.
     */
    private synchronized void addDevice(String locationURL, String uuid, String localAddress)
    {
        if (locationURL == null || "".equals(locationURL.trim()))
        {
            LogTool.d("LocationURL is error.");
            return;
        }

        try
        {
            URL locationUrl = new URL(locationURL);
            Parser parser = UPnP.getXMLParser();
            Node rootNode = parser.parse(locationUrl);
            Device rootDev = getDevice(rootNode);
            if (rootDev == null)
                return;
            if (rootDev.isDeviceType(ST.MULTISCREEN_DEVICE) == false)
            {
                // CN:通知类型是NT: upnp:rootdevice，非多屏设备不加入NodeList进行维护。
                return;
            }

            // FIXME CN:get default ssdpPacket.
            SSDPPacket ssdpPacket = getDefaultSsdp(locationURL, uuid, localAddress);

            // CN:确认设备是否存在
            String usn = ssdpPacket.getUSN();
            String udn = USN.getUDN(usn);
            Device dev = getDevice(udn);
            if (dev != null)
            {
                dev.setSSDPPacket(ssdpPacket);
                performRefreshDeviceListener(dev);
                return;
            }

            rootDev.setSSDPPacket(ssdpPacket);
            addDevice(rootNode);
            performAddDeviceListener(rootDev);
        }
        catch (MalformedURLException me)
        {
            Debug.warning(locationURL);
            Debug.warning(me);
        }
        catch (ParserException pe)
        {
            Debug.warning(locationURL);
            Debug.warning(pe);
        }
    }

    private Device getDevice(Node rootNode)
    {
        if (rootNode == null)
            return null;
        // Begin modify for MultiScreen.
        rootNode.lock();
        Node devNode = rootNode.getNode(Device.ELEM_NAME);
        if (devNode == null)
        {
            rootNode.unlock();
            return null;
        }
        rootNode.unlock();
        // End modify for MultiScreen.
        return new Device(rootNode, devNode);
    }

    public DeviceList getDeviceList()
    {
        DeviceList devList = new DeviceList();
        // Begin modify for MultiScreen.
        devNodeList.lock();
        int nRoots = devNodeList.size();
        for (int n=0; n<nRoots; n++) {
            Node rootNode = devNodeList.getNode(n);
            Device dev = getDevice(rootNode);
            if (dev == null)
                continue;
            devList.add(dev);
        }
        devNodeList.unlock();
        // End modify for MultiScreen.
        return devList;
    }

    public Device getDevice(String name)
    {
        // Modify for multiscreen.
        int nRoots = devNodeList.size();
        for (int n = 0; n < nRoots; n++)
        {
            Node rootNode = devNodeList.getNode(n);
            Device dev = getDevice(rootNode);
            if (dev == null)
                continue;

            if (dev.isDevice(name) == true)
            {
                return dev;
            }
            Device cdev = dev.getDevice(name);
            if (cdev != null)
            {
                return cdev;
            }
        }
        // End modify for multiscreen.
        return null;
    }

    public boolean hasDevice(String name)
    {
        return (getDevice(name) != null) ? true : false;
    }

    private void removeDevice(Node rootNode)
    {
        // Thanks for Oliver Newell (2004/10/16)
        // Invoke device removal listener prior to actual removal so Device node
        // remains valid for the duration of the listener (application may want
        // to access the node)
        Device dev = getDevice(rootNode);
        if( dev != null && dev.isRootDevice() )
            performRemoveDeviceListener( dev );

        devNodeList.remove(rootNode);
    }

    protected void removeDevice(Device dev)
    {
        if (dev == null)
            return;
        removeDevice(dev.getRootNode());
    }

    protected void removeDevice(String name)
    {
        // Modify for MultiScreen.
        devNodeList.lock();
        Device dev = getDevice(name);
        removeDevice(dev);
        devNodeList.unlock();
        // End modify for MultiScreen.
    }

    private void removeDevice(SSDPPacket packet)
    {
        if (packet.isByeBye() == false)
            return;
        String usn = packet.getUSN();
        String udn = USN.getUDN(usn);
        // Begin modify for MultiScreen
        lock();
        removeDevice(udn);
        unlock();
        // End modify for MultiScreen
    }

    ////////////////////////////////////////////////
    //    Expired Device
    ////////////////////////////////////////////////

    private Disposer deviceDisposer;
    private long expiredDeviceMonitoringInterval;

    public void removeExpiredDevices()
    {
        DeviceList devList = getDeviceList();
        int devCnt = devList.size();
        Device dev[] = new Device[devCnt];
        for (int n=0; n<devCnt; n++)
            dev[n] = devList.getDevice(n);
        for (int n=0; n<devCnt; n++) {
            if (dev[n].isExpired() == true) {
                Debug.message("Expired device = " + dev[n].getFriendlyName());
                removeDevice(dev[n]);
            }
        }
    }

    public void setExpiredDeviceMonitoringInterval(long interval)
    {
        expiredDeviceMonitoringInterval = interval;
    }

    public long getExpiredDeviceMonitoringInterval()
    {
        return expiredDeviceMonitoringInterval;
    }

    public void setDeviceDisposer(Disposer disposer)
    {
        deviceDisposer = disposer;
    }

    public Disposer getDeviceDisposer()
    {
        return deviceDisposer;
    }

    ////////////////////////////////////////////////
    //    Notify
    ////////////////////////////////////////////////

    private ListenerList deviceNotifyListenerList = new ListenerList();

    public void addNotifyListener(NotifyListener listener)
    {
        deviceNotifyListenerList.add(listener);
    }

    public void removeNotifyListener(NotifyListener listener)
    {
        deviceNotifyListenerList.remove(listener);
    }

    public void performNotifyListener(SSDPPacket ssdpPacket)
    {
        int listenerSize = deviceNotifyListenerList.size();
        for (int n=0; n<listenerSize; n++) {
            NotifyListener listener = (NotifyListener)deviceNotifyListenerList.get(n);
            try{
                listener.deviceNotifyReceived(ssdpPacket);
            }catch(Exception e){
                Debug.warning("NotifyListener returned an error:", e);
            }
        }
    }

    ////////////////////////////////////////////////
    //    SearchResponse
    ////////////////////////////////////////////////

    private ListenerList deviceSearchResponseListenerList = new ListenerList();

    public void addSearchResponseListener(SearchResponseListener listener)
    {
        deviceSearchResponseListenerList.add(listener);
    }

    public void removeSearchResponseListener(SearchResponseListener listener)
    {
        deviceSearchResponseListenerList.remove(listener);
    }

    public void performSearchResponseListener(SSDPPacket ssdpPacket)
    {
        int listenerSize = deviceSearchResponseListenerList.size();
        for (int n=0; n<listenerSize; n++) {
            SearchResponseListener listener = (SearchResponseListener)deviceSearchResponseListenerList.get(n);
            try{
                listener.deviceSearchResponseReceived(ssdpPacket);
            }catch(Exception e){
                Debug.warning("SearchResponseListener returned an error:", e);
            }


        }
    }

    /////////////////////////////////////////////////////////////////////
    // Device status changes (device added or removed)
    // Applications that support the DeviceChangeListener interface are
    // notified immediately when a device is added to, or removed from,
    // the control point.
    /////////////////////////////////////////////////////////////////////

    ListenerList deviceChangeListenerList = new ListenerList();

    public void addDeviceChangeListener(DeviceChangeListener listener)
    {
        deviceChangeListenerList.add(listener);
    }

    public void removeDeviceChangeListener(DeviceChangeListener listener)
    {
        deviceChangeListenerList.remove(listener);
    }

    public void performAddDeviceListener( Device dev )
    {
        int listenerSize = deviceChangeListenerList.size();
        for (int n=0; n<listenerSize; n++) {
            DeviceChangeListener listener = (DeviceChangeListener)deviceChangeListenerList.get(n);
            listener.deviceAdded( dev );
        }
    }

    public void performRemoveDeviceListener( Device dev )
    {
        int listenerSize = deviceChangeListenerList.size();
        for (int n=0; n<listenerSize; n++) {
            DeviceChangeListener listener = (DeviceChangeListener)deviceChangeListenerList.get(n);
            listener.deviceRemoved( dev );
        }
    }

    public void performRefreshDeviceListener( Device dev )
    {
        int listenerSize = deviceChangeListenerList.size();
        for (int n=0; n<listenerSize; n++) {
            DeviceChangeListener listener = (DeviceChangeListener)deviceChangeListenerList.get(n);
            listener.deviceRefreshed( dev );
        }
    }

    ////////////////////////////////////////////////
    //    SSDPPacket
    ////////////////////////////////////////////////

    public void notifyReceived(SSDPPacket packet)
    {
        // Begin modify for MultiScreen. remove (packet.isRootDevice() == true)
        if (packet.isHiMultiScreenDevice() == true)
        {
            if (packet.isAlive() == true)
            {
                addDevice(packet);
            }
            else if (packet.isByeBye() == true)
            {
                removeDevice(packet);
            }
            performNotifyListener(packet);
        }
        // End modify for MultiScreen.
        //performNotifyListener(packet);
    }

    public void searchResponseReceived(SSDPPacket packet)
    {
        // Begin modify for MultiScreen.
        if (packet.isHiMultiScreenDevice() == true)
        {
            addDevice(packet);
            performSearchResponseListener(packet);
        }
        // End modify for MultiScreen.
        //performSearchResponseListener(packet);
    }

    ////////////////////////////////////////////////
    //    M-SEARCH
    ////////////////////////////////////////////////

    private int searchMx = SSDP.DEFAULT_MSEARCH_MX;

    public int getSearchMx()
    {
        return searchMx;
    }

    public void setSearchMx(int mx)
    {
        searchMx = mx;
    }

    public void search(String target, int mx)
    {
        SSDPSearchRequest msReq = new SSDPSearchRequest(target, mx);
        SSDPSearchResponseSocketList ssdpSearchResponseSocketList = getSSDPSearchResponseSocketList();
        ssdpSearchResponseSocketList.post(msReq);
    }

    public void search(String target)
    {
        search(target, SSDP.DEFAULT_MSEARCH_MX);
    }

    public void search()
    {
        search(ST.ROOT_DEVICE, SSDP.DEFAULT_MSEARCH_MX);
    }

    /**
     * Search the device by location URL.<br>
     * @param locationURL URL of description.xml on STB.<br>
     *        like: "http://192.168.0.3:49152/description.xml"
     * @param uuid UUID of device.<br>
     *        like: "cc6g32df-aaaa-22c3-e029-0AAC26386B77"
     * @param localAddress local IP address.
     */
    public void searchByUrl(String locationURL, String uuid, String localAddress)
    {
        addDevice(locationURL, uuid, localAddress);
    }

    ////////////////////////////////////////////////
    //    EventSub HTTPServer
    ////////////////////////////////////////////////

    private HTTPServerList httpServerList = new HTTPServerList();

    private HTTPServerList getHTTPServerList()
    {
        return httpServerList;
    }

    public void httpRequestRecieved(HTTPRequest httpReq)
    {
        if (Debug.isOn() == true)
            httpReq.print();

        // Thanks for Giordano Sassaroli <sassarol@cefriel.it> (09/08/03)
        if (httpReq.isNotifyRequest() == true) {
            NotifyRequest notifyReq = new NotifyRequest(httpReq);
            String uuid = notifyReq.getSID();
            long seq = notifyReq.getSEQ();
            PropertyList props = notifyReq.getPropertyList();
            int propCnt = props.size();
            for (int n = 0; n < propCnt; n++) {
                Property prop = props.getProperty(n);
                String varName = prop.getName();
                String varValue = prop.getValue();
                performEventListener(uuid, seq, varName, varValue);
            }
            httpReq.returnOK();
            return;
         }

        httpReq.returnBadRequest();
    }

    ////////////////////////////////////////////////
    //    Event Listener
    ////////////////////////////////////////////////

    private ListenerList eventListenerList = new ListenerList();

    public void addEventListener(EventListener listener)
    {
        eventListenerList.add(listener);
    }

    public void removeEventListener(EventListener listener)
    {
        eventListenerList.remove(listener);
    }

    public void performEventListener(String uuid, long seq, String name, String value)
    {
        int listenerSize = eventListenerList.size();
        for (int n=0; n<listenerSize; n++) {
            EventListener listener = (EventListener)eventListenerList.get(n);
            listener.eventNotifyReceived(uuid, seq, name, value);
        }
    }

    ////////////////////////////////////////////////
    //    Subscription
    ////////////////////////////////////////////////

    private String eventSubURI = DEFAULT_EVENTSUB_URI;

    public String getEventSubURI()
    {
        return eventSubURI;
    }

    public void setEventSubURI(String url)
    {
        eventSubURI = url;
    }

    private String getEventSubCallbackURL(String host)
    {
        return HostInterface.getHostURL(host, getHTTPPort(), getEventSubURI());
    }

    public boolean subscribe(Service service, long timeout)
    {
        if (service.isSubscribed() == true) {
            String sid = service.getSID();
            return subscribe(service, sid, timeout);
        }

        Device rootDev = service.getRootDevice();
        if (rootDev == null)
            return false;
        String ifAddress = rootDev.getInterfaceAddress();
        SubscriptionRequest subReq = new SubscriptionRequest();
        subReq.setSubscribeRequest(service, getEventSubCallbackURL(ifAddress), timeout);
        SubscriptionResponse subRes = subReq.post();
        if (subRes.isSuccessful() == true) {
            service.setSID(subRes.getSID());
            service.setTimeout(subRes.getTimeout());
            // LogTool.d("timeout:"+subRes.getTimeout());
            return true;

        }
        service.clearSID();
        return false;
    }

    public boolean subscribe(Service service)
    {
        // FIXME CN:协商可能失败，服务端是300s.
        return subscribe(service, Subscription.INFINITE_VALUE);
    }

    public boolean subscribe(Service service, String uuid, long timeout)
    {
        SubscriptionRequest subReq = new SubscriptionRequest();
        subReq.setRenewRequest(service, uuid, timeout);
        if (Debug.isOn() == true)
            subReq.print();
        SubscriptionResponse subRes = subReq.post();
        if (Debug.isOn() == true)
            subRes.print();
        if (subRes.isSuccessful() == true) {
            service.setSID(subRes.getSID());
            service.setTimeout(subRes.getTimeout());
            return true;
        }
        service.clearSID();
        return false;
    }

    public boolean subscribe(Service service, String uuid)
    {
        return subscribe(service, uuid, Subscription.INFINITE_VALUE);
    }

    public boolean isSubscribed(Service service)
    {
        if (service == null)
            return false;
        return service.isSubscribed();
    }

    public boolean unsubscribe(Service service)
    {
        SubscriptionRequest subReq = new SubscriptionRequest();
        subReq.setUnsubscribeRequest(service);
        SubscriptionResponse subRes = subReq.post();
        if (subRes.isSuccessful() == true) {
            service.clearSID();
            return true;
        }
        return false;
    }

    public void unsubscribe(Device device)
    {
        ServiceList serviceList = device.getServiceList();
        int serviceCnt = serviceList.size();
        for (int n=0; n<serviceCnt; n++) {
            Service service = serviceList.getService(n);
            if (service.hasSID() == true)
                unsubscribe(service);
        }

        DeviceList childDevList = device.getDeviceList();
        int childDevCnt = childDevList.size();
        for (int n=0; n<childDevCnt; n++) {
            Device cdev = childDevList.getDevice(n);
            unsubscribe(cdev);
        }
    }

    public void unsubscribe()
    {
        DeviceList devList = getDeviceList();
        int devCnt = devList.size();
        for (int n=0; n<devCnt; n++) {
            Device dev = devList.getDevice(n);
            unsubscribe(dev);
        }
    }

    ////////////////////////////////////////////////
    //    getSubscriberService
    ////////////////////////////////////////////////

    public Service getSubscriberService(String uuid)
    {
        DeviceList devList = getDeviceList();
        int devCnt = devList.size();
        for (int n=0; n<devCnt; n++) {
            Device dev = devList.getDevice(n);
            Service service = dev.getSubscriberService(uuid);
            if (service != null)
                return service;
        }
        return null;
    }

    ////////////////////////////////////////////////
    //    getSubscriberService
    ////////////////////////////////////////////////

    public void renewSubscriberService(Device dev, long timeout)
    {
        ServiceList serviceList = dev.getServiceList();
        int serviceCnt = serviceList.size();
        for (int n=0; n<serviceCnt; n++) {
            Service service = serviceList.getService(n);
            if (service.isSubscribed() == false)
                continue;
            String sid = service.getSID();
            boolean isRenewed = subscribe(service, sid, timeout);
            if (isRenewed == false)
                subscribe(service, timeout);
        }

        DeviceList cdevList = dev.getDeviceList();
        int cdevCnt = cdevList.size();
        for (int n=0; n<cdevCnt; n++) {
            Device cdev = cdevList.getDevice(n);
            renewSubscriberService(cdev, timeout);
        }
    }

    public void renewSubscriberService(long timeout)
    {
        DeviceList devList = getDeviceList();
        int devCnt = devList.size();
        for (int n=0; n<devCnt; n++) {
            Device dev = devList.getDevice(n);
            renewSubscriberService(dev, timeout);
        }
    }

    public void renewSubscriberService()
    {
        renewSubscriberService(Subscription.INFINITE_VALUE);
    }

    ////////////////////////////////////////////////
    //    Subscriber
    ////////////////////////////////////////////////

    private RenewSubscriber renewSubscriber;

    public void setRenewSubscriber(RenewSubscriber sub)
    {
        renewSubscriber = sub;
    }

    public RenewSubscriber getRenewSubscriber()
    {
        return renewSubscriber;
    }

    ////////////////////////////////////////////////
    //    run
    ////////////////////////////////////////////////

    public boolean start(String target, int mx)
    {
        stop();

        ////////////////////////////////////////
        // HTTP Server
        ////////////////////////////////////////

        int retryCnt = 0;
        int bindPort = getHTTPPort();
        HTTPServerList httpServerList = getHTTPServerList();
        while (httpServerList.open(bindPort) == false) {
            retryCnt++;
            if (UPnP.SERVER_RETRY_COUNT < retryCnt)
                return false;
            setHTTPPort(bindPort + 1);
            bindPort = getHTTPPort();
        }
        httpServerList.addRequestListener(this);
        httpServerList.start();

        ////////////////////////////////////////
        // Notify Socket
        ////////////////////////////////////////

        SSDPNotifySocketList ssdpNotifySocketList = getSSDPNotifySocketList();
        if (ssdpNotifySocketList.open() == false)
            return false;
        ssdpNotifySocketList.setControlPoint(this);
        ssdpNotifySocketList.start();

        ////////////////////////////////////////
        // SeachResponse Socket
        ////////////////////////////////////////

        int ssdpPort = getSSDPPort();
        retryCnt = 0;
        SSDPSearchResponseSocketList ssdpSearchResponseSocketList = getSSDPSearchResponseSocketList();
        while (ssdpSearchResponseSocketList.open(ssdpPort) == false) {
            retryCnt++;
            if (UPnP.SERVER_RETRY_COUNT < retryCnt)
                return false;
            setSSDPPort(ssdpPort + 1);
            ssdpPort = getSSDPPort();
        }
        ssdpSearchResponseSocketList.setControlPoint(this);
        ssdpSearchResponseSocketList.start();

        ////////////////////////////////////////
        // search root devices
        ////////////////////////////////////////

        search(target, mx);

        ////////////////////////////////////////
        // Disposer
        ////////////////////////////////////////

        Disposer disposer = new Disposer(this);
        setDeviceDisposer(disposer);
        disposer.start();

        ////////////////////////////////////////
        // Subscriber
        ////////////////////////////////////////

        if (isNMPRMode() == true) {
            RenewSubscriber renewSub = new RenewSubscriber(this);
            setRenewSubscriber(renewSub);
            renewSub.start();
        }

        return true;
    }

    public boolean start(String target)
    {
        return start(target, SSDP.DEFAULT_MSEARCH_MX);
    }

    public boolean start()
    {
        return start(ST.ROOT_DEVICE, SSDP.DEFAULT_MSEARCH_MX);
    }

    public boolean stop()
    {
        unsubscribe();

        SSDPNotifySocketList ssdpNotifySocketList = getSSDPNotifySocketList();
        ssdpNotifySocketList.stop();
        ssdpNotifySocketList.close();
        ssdpNotifySocketList.clear();

        SSDPSearchResponseSocketList ssdpSearchResponseSocketList = getSSDPSearchResponseSocketList();
        ssdpSearchResponseSocketList.stop();
        ssdpSearchResponseSocketList.close();
        ssdpSearchResponseSocketList.clear();

        HTTPServerList httpServerList = getHTTPServerList();
        httpServerList.stop();
        httpServerList.close();
        httpServerList.clear();

        ////////////////////////////////////////
        // Disposer
        ////////////////////////////////////////

        Disposer disposer = getDeviceDisposer();
        if (disposer != null) {
            disposer.stop();
            setDeviceDisposer(null);
        }

        ////////////////////////////////////////
        // Subscriber
        ////////////////////////////////////////

        RenewSubscriber renewSub = getRenewSubscriber();
        if (renewSub != null) {
            renewSub.stop();
            setRenewSubscriber(null);
        }

        return true;
    }

    ////////////////////////////////////////////////
    //    userData
    ////////////////////////////////////////////////

    private Object userData = null;

    public void setUserData(Object data)
    {
        userData = data;
    }

    public Object getUserData()
    {
        return userData;
    }

    ////////////////////////////////////////////////
    //    print
    ////////////////////////////////////////////////

    public void print()
    {
        DeviceList devList = getDeviceList();
        int devCnt = devList.size();
        Debug.message("Device Num = " + devCnt);
        for (int n=0; n<devCnt; n++) {
            Device dev = devList.getDevice(n);
            Debug.message("[" + n + "] " + dev.getFriendlyName() + ", " + dev.getLeaseTime() + ", " + dev.getElapsedTime());
        }
    }

    /**
     * Get default SSDP packet.<br>
     * @param locationURL
     * @param uuid
     * @param localAddress
     * @return
     */
    private SSDPPacket getDefaultSsdp(String locationURL, String uuid, String localAddress)
    {
        // FIXME CN:构造ssdpPacket data内容。
        StringBuffer ssdpContent = new StringBuffer("NOTIFY * HTTP/1.1\n");
        ssdpContent.append("HOST: 239.255.255.250:1900\n");
        ssdpContent.append("CACHE-CONTROL: max-age=1800\n");
        // ssdpContent.append("LOCATION: http://192.168.0.3:49152/description.xml\n");
        ssdpContent.append("LOCATION: ");
        ssdpContent.append(locationURL);
        ssdpContent.append("\n");
        ssdpContent.append("OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\n");
        ssdpContent.append("01-NLS: c663375c-1dd1-11b2-b12f-c5eca95ebebe\n");
        ssdpContent.append("NT: urn:schemas-upnp-org:device:HiMultiScreenServerDevice:1\n");
        ssdpContent.append("NTS: ssdp:alive\n");
        ssdpContent
            .append("SERVER: Linux/3.10.0_s40 UPnP/1.0 Portable SDK for UPnP devices/1.6.12\n");
        ssdpContent.append("X-User-Agent: redsonic\n");
        // ssdpContent.append("USN: uuid:cc6g32df-aaaa-22c3-e029-0AAC26386B77::urn:schemas-upnp-org:device:HiMultiScreenServerDevice:1");
        ssdpContent.append("USN: uuid:");
        ssdpContent.append(uuid);
        ssdpContent.append("::urn:schemas-upnp-org:device:HiMultiScreenServerDevice:1");
        String packetData = ssdpContent.toString();

        // CN:构造ssdpPacket
        byte ssdvRecvBuf[] = new byte[SSDP.RECV_MESSAGE_BUFSIZE];
        SSDPPacket ssdpPacket = new SSDPPacket(ssdvRecvBuf, ssdvRecvBuf.length);
        ssdpPacket.setLocalAddress(localAddress); //
        ssdpPacket.getDatagramPacket().setData(packetData.getBytes());
        ssdpPacket.setTimeStamp(System.currentTimeMillis());
        return ssdpPacket;
    }
}
